﻿// Bravura : A music composition and audio synthesis framework.
//
// Copyright © 2011 Paul Vanukoff (pvanukoff@gmail.com)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Xaml;
using Bravura.Core.Common;
using Bravura.Core.MusicLib.Model;
using Bravura.Core.SynthLib;

namespace Bravura.Core.MusicLib
{
	/// <summary>
	/// Music engine implementation.
	/// </summary>
	public class MusicEngine : IMusicEngine, ISynthEngineHost
	{
		#region Fields

		private DirectoryInfo _outputPath = null;
		private IMusicEngineHost _host = null;
		private int _sampleRate = 48000;
		private int _ticksPerBeat = 12;
		private double _beatsPerMin = 120.0;
		private double _ticksPerSec = 24.0;
		private double _samplesPerTick = 2000.0;
		private double _sampleCountDbl = 0.0;
		private int _sampleCountInt = 0;
		private MusicProject _project = null;
		private List<ISynthEngine> _synthList = new List<ISynthEngine>();
		private Dictionary<int, ISynthEngine> _synthDic = new Dictionary<int, ISynthEngine>();
		private Dictionary<int, Element> _elementDic = new Dictionary<int, Element>();
		private Dictionary<int, Block> _blockDic = new Dictionary<int, Block>();
		private string _homeDir = null;

		#endregion

		/// <summary>
		/// Create a new MusicEngine instance.
		/// </summary>
		public MusicEngine(IMusicEngineHost host, int sampleRate)
		{
			_host = host;
			_sampleRate = sampleRate;
		}

		#region IMusicEngine Implementation

		/// <summary>
		/// Load a project from disk.
		/// </summary>
		public void Load(FileInfo projectFile)
		{
			object obj = XamlServices.Load(projectFile.FullName);
			if (obj is MusicProject)
			{
				string oldDir = Directory.GetCurrentDirectory();
				Directory.SetCurrentDirectory(_homeDir = projectFile.DirectoryName);
				this.Load(obj as MusicProject);
				Directory.SetCurrentDirectory(oldDir);
			}
		}

		/// <summary>
		/// Load a project from a MusicProject object.
		/// </summary>
		public void Load(MusicProject project)
		{
			_project = project;
			_outputPath = new DirectoryInfo(_project.Name);

			// some synths / plugins might need to know the tempo right away
			SetTempo(_project.BeatsPerMin);

			// add synth engines
			foreach (SynthProjectInfo info in _project.SynthProjects)
			{
				ISynthEngine eng = new SynthEngine(this, _sampleRate);
				eng.Load(new FileInfo(info.FileName));
				_synthDic[info.Number] = eng;
				_synthList.Add(eng);
			}

			// populate element lookup dictionary
			foreach (Element element in _project.ElementList)
			{
				_elementDic[element.Num] = element;
			}

			// populate block lookup dictionary
			foreach (Block block in _project.BlockList)
			{
				_blockDic[block.Num] = block;
			}
		}

		/// <summary>
		/// Render the currently loaded project.
		/// </summary>
		public void Render()
		{
			this.Render(_project.StartBeat, _project.EndBeat);
		}

		/// <summary>
		/// Render the currently loaded project.
		/// </summary>
		public void Render(int from, int to)
		{
			_ticksPerBeat = _project.TicksPerBeat;

			SetTempo(_project.BeatsPerMin);

			// report starting
			if (_host != null)
			{
				_host.LogMessage(this, TraceLevel.Info, "Rendering...");
			}

			// init/open the synth engines
			foreach (ISynthEngine synth in _synthList)
			{
				synth.Open();
			}

			#region Rendering Loop

			for (int beat = from; beat < to; beat++)
			{
				if (_host != null)
				{
					// ensure we should keep rendering
					if (_host.ShouldContinue())
					{
						// yes ? report progress
						_host.LogProgress(beat - from, to - from);
					}
					else
					{
						// no ? bail out
						break;
					}
				}

				// process and render all ticks in beat
				for (int tick = 0; tick < _ticksPerBeat; tick++)
				{
					// execute commands at this beat and tick
					processElement(_project, beat, tick);

					// calc next ideal sample count
					_sampleCountDbl += _samplesPerTick;

					// calc how many samples we should render
					int count = (int)(_sampleCountDbl - _sampleCountInt);

					// run the synth engines
					foreach (ISynthEngine synth in _synthList)
					{
						synth.Process(count);
					}

					// update actual sample count
					_sampleCountInt += count;
				}

				// wrap sample counts as neeeded
				while (_sampleCountDbl > _sampleRate)
				{
					_sampleCountDbl -= _sampleRate;
					_sampleCountInt -= _sampleRate;
				}
			}

			#endregion

			// close the synth engines
			foreach (ISynthEngine synth in _synthList)
			{
				synth.Close();
			}

			// report completion
			if (_host != null)
			{
				_host.LogMessage(this, TraceLevel.Info, "Rendering finished.");
			}
		}

		/// <summary>
		/// Sets the tempo (bpm).
		/// </summary>
		public void SetTempo(double bpm)
		{
			_beatsPerMin = bpm;
			_ticksPerSec = _ticksPerBeat * _beatsPerMin / 60.0;
			_samplesPerTick = _sampleRate / _ticksPerSec;
		}

		#endregion

		#region ISynthEngineHost Implementation

		/// <summary>
		/// Allows a hosted synth engine to log a message.
		/// </summary>
		public void LogMessage(ISynthEngine source, TraceLevel severity, string message)
		{
			if (_host != null)
			{
				_host.LogMessage(this, severity, message);
			}
		}

		/// <summary>
		/// Returns the output path.
		/// </summary>
		public DirectoryInfo GetOutputPath()
		{
			return _outputPath;
		}

		/// <summary>
		/// Returns the tempo (in bpm).
		/// </summary>
		public double GetTempo()
		{
			return _beatsPerMin;
		}

		#endregion

		#region Private

		private void processElement(Element element, int beat, int tick)
		{
			// process element instances
			foreach (ElementInstance ei in element.ElementInstances)
			{
				Element e = _elementDic[ei.ElementNum];

				int b0 = ei.Beat;
				int b1 = b0 + e.Length;

				if (beat >= b0 && beat < b1 || beat == b1 && tick == 0)
				{
					processElement(e, beat - b0, tick);
				}
			}

			// process block instances
			foreach (BlockInstance bi in element.BlockInstances)
			{
				Block b = _blockDic[bi.BlockNum];

				int b0 = bi.Beat;
				int b1 = b0 + b.Length;

				if (beat >= b0 && beat < b1 || beat == b1 && tick == 0)
				{
					processBlock(b, (beat - b0) * _ticksPerBeat + tick, bi.Modifiers);
				}
			}
		}

		private void processBlock(Block block, int tick, IEnumerable<IModifier> modifiers)
		{
			ICommandTarget target = null;

			// determine target
			if (block.SynthNum.HasValue)
			{
				ISynthEngine synth = _synthDic[block.SynthNum.Value];
				if (block.PluginNum.HasValue)
				{
					target = synth.GetPlugin(block.PluginNum.Value);
				}
				else
				{
					target = synth;
				}
			}
			else
			{
				target = this;
			}

			// TODO : CACHE AND REUSE MODIFIED DIRECTIVES

			// execute directives against the target
			foreach (IDirective d in block.Directives)
			{
				IDirective x = d;
				foreach (IModifier mod in modifiers)
				{
					x = mod.Modify(x);
				}
				x.Execute(target, tick);
			}
		}

		#endregion
	}
}
